﻿//==============================================================================  
//Copyright (C) 2012-2015 9UN.ORG. All rights reserved. 
//GUID：3a0169c4-e639-43a7-a36d-761e873252f7
//CLR Version: 4.0.30319.18033
//Code Author：Kevin Wang
//Contact：Email(Admin@9un.org),QQ(265382 or 74344)
//Filename：AirPlayLisenter
//Namespace：RuanRang.Core.Utility
//Functions：AirPlayLisenter  
//Created by Kevin Wang at 2013/2/4 20:32:09 http://blog.9un.org
//============================================================================== 
using System;
using System.IO;
using System.Net;
using System.Runtime.InteropServices;
using System.Threading;

namespace RuanRang.Core.Utility
{
    //TODO 需要支持文件目录
    //TODO 需要支持授权验证码
    public class AirPlayLisenter : IDisposable
    {
        #region Fields
        private HttpListener _httpListener;

        /// <summary>
        /// Gets the port.
        /// </summary>
        public int Port { get; private set; }

        /// <summary>
        /// Gets or sets the Current File to be Streamed.
        /// </summary>
        /// <value>
        /// The current file.
        /// </value>
        public string CurrentFolder { get; set; }

        /// <summary>
        /// Gets or sets the length of the output stream buffer.
        /// </summary>
        /// <value>The length of the output stream buffer.</value>
        public long OutputStreamBufferLength { get; set; }

        #endregion Fields
        /// <summary>
        /// Initializes a new instance of the <see cref="AirPlayLisenter" /> class.
        /// </summary>
        /// <param name="port">The port.</param>
        /// <param name="workFolder">The work folder.</param>
        /// <param name="outputStreamBufferLength">Length of the output stream buffer.</param>
        /// <exception cref="System.IO.IOException">he target folder dose not exist,  Path: + CurrentFolder</exception>
        public AirPlayLisenter(int port, string workFolder, long outputStreamBufferLength = 512 * 1024)
        {
            CurrentFolder = workFolder.TrimEnd('\\');
            if (!Directory.Exists(CurrentFolder))
            {
                throw new IOException("The target folder dose not exist,  Path:" + CurrentFolder);
            }
            _httpListener = new HttpListener();
            Port = port;
            OutputStreamBufferLength = outputStreamBufferLength;
            var urlPrefix = "http://*:" + Port + "/";
            _httpListener.Prefixes.Add(urlPrefix);
#if DEBUG
            Console.WriteLine("Uri Prefix: {0}", urlPrefix);
#endif
        }

        /// <summary>
        /// Starts this instance.
        /// </summary>
        public void Start()
        {
#if DEBUG
            Console.WriteLine("Start Lisenter...");
#endif
            _httpListener.Start();
            var result = _httpListener.BeginGetContext(ListenerCallback, _httpListener);
            _httpListener.IgnoreWriteExceptions = true;
#if DEBUG
            Console.WriteLine("Async State: {0}", result.AsyncState);
#endif
        }

        /// <summary>
        /// Stops this instance.
        /// </summary>
        public void Stop()
        {
            _httpListener.Stop();
        }

        /// <summary>
        /// Listener event callback
        /// </summary>
        /// <param name="result">The result.</param>
        private void ListenerCallback(IAsyncResult result)
        {
            ThreadPool.QueueUserWorkItem(state =>
                    {
#if DEBUG
                        Console.WriteLine("************************************************************");
                        Console.WriteLine("Get Request!\r\n");
#endif
                        Process(state as IAsyncResult);
                    }
                , result);
        }

        /// <summary>
        /// Listener event callback
        /// </summary>
        /// <param name="result">The result.</param>
        private void Process(IAsyncResult result)
        {
            var listener = (HttpListener)result.AsyncState;
            // Call EndGetContext to complete the asynchronous operation.
            var context = listener.EndGetContext(result);
            _httpListener.BeginGetContext(ListenerCallback, _httpListener);
            //Obtain the request object.
            var request = context.Request;

            var responseConn = "keep-alive";
            //name of key
            var connKey = HttpResponseHeader.Connection.ToString();
            var connValue = request.Headers.Get(connKey);
            if (!string.IsNullOrEmpty(connValue))
            {
                responseConn = connValue;
            }
#if DEBUG
            Console.WriteLine("Request Headers: ");
            Console.WriteLine(request.Headers);
            Console.WriteLine("---------------------------------");
#endif
            // Obtain the response object.
            var response = context.Response;

            var absolutePath = request.Url.AbsolutePath.Replace("/", "\\");
            var filePath = CurrentFolder + absolutePath;
            if (!File.Exists(filePath))
            {
                response.StatusCode = 404;
                response.Close();
                return;
            }
            var fileInfo = new FileInfo(filePath);
            if (fileInfo.Length <= 0)
            {
                response.StatusCode = 505;
                response.Close();
                return;
            }
            // set response headers.
            response.Headers[HttpResponseHeader.Server] = "Course Player Server/1.0";
            response.Headers[HttpResponseHeader.AcceptRanges] = "bytes";
            response.ContentType = MatchContentType.Match(fileInfo.Extension);
            response.Headers[HttpResponseHeader.CacheControl] = "no-cache";
            response.Headers[HttpResponseHeader.Connection] = responseConn;
            if (request.Headers["Range"] != null)
            {
                ProcessRangeRequest(request, response, fileInfo);
                return;
            }
            ProcessNormalRequest(response, fileInfo);
        }

        /// <summary>
        /// Processes the normal request.
        /// </summary>
        /// <param name="response">The response.</param>
        /// <param name="targetFileInfo">The target file info.</param>
        private void ProcessNormalRequest(HttpListenerResponse response, FileInfo targetFileInfo)
        {
            var targetFileLength = targetFileInfo.Length;
            response.StatusCode = (int)HttpStatusCode.OK;
            response.KeepAlive = true;
            response.ContentLength64 = targetFileLength;
            using (var fileStream = new FileStream(targetFileInfo.FullName, FileMode.Open, FileAccess.Read, FileShare.Read))
            {
                SendBytes(fileStream, 0, OutputStreamBufferLength, response.OutputStream, targetFileLength);
            }
            response.Close();
        }

        /// <summary>
        /// Processes the range Request.
        /// </summary>
        /// <param name="request">The request.</param>
        /// <param name="response">The response.</param>
        /// <param name="targetFileInfo">The target file info.</param>
        private void ProcessRangeRequest(HttpListenerRequest request, HttpListenerResponse response, FileInfo targetFileInfo)
        {
            var targetFileLength = targetFileInfo.Length;
            var requestedRange = request.Headers["Range"].Replace("bytes=", string.Empty);
            var array = requestedRange.Split('-');

            //set value
            long startRange;
            long endRange;
            if (array.Length <= 0)
            {
                startRange = 0;
                endRange = 0;
            }
            else
            {
                //extract value from headers. get the value by the Key: "Range".
                startRange = long.TryParse(array[0], out startRange) ? startRange : 0;
                var endValue = array.Length >= 2 ? array[1].Trim() : string.Empty;
                endRange = string.IsNullOrEmpty(endValue) ? 0 : (long.TryParse(endValue, out endRange) ? endRange : 0);
            }
            startRange = startRange > targetFileLength ? targetFileLength : startRange;
            endRange = endRange > targetFileLength ? targetFileLength : endRange;

            // Construct a response.
            response.StatusCode = (int)HttpStatusCode.PartialContent;
            response.KeepAlive = true;
            if (endRange == 0 || endRange < startRange)
            {
                endRange = targetFileLength;
            }
            var requestLength = endRange - startRange;
            if (requestLength == 0)
            {
                requestLength = 1;
            }
            response.Headers[HttpResponseHeader.ContentRange] = "bytes " + startRange + "-" + endRange + "/" + (targetFileLength + 1);
            response.ContentLength64 = requestLength;
            using (var fileStream = new FileStream(targetFileInfo.FullName, FileMode.Open, FileAccess.Read, FileShare.Read))
            {
                SendBytes(fileStream, startRange, OutputStreamBufferLength, response.OutputStream, requestLength);
            }
            response.Close();
        }

        /// <summary>
        /// Sends the bytes.
        /// </summary>
        /// <param name="inputStream">The input stream.</param>
        /// <param name="offset">The offset.</param>
        /// <param name="bufferLength">Length of the buffer.</param>
        /// <param name="outputStream">The output stream.</param>
        /// <param name="outputTotalLength">Total length of the output.</param>
        protected void SendBytes(Stream inputStream, long offset, long bufferLength, Stream outputStream,
                                      long outputTotalLength)
        {
            var readBuffer = new byte[bufferLength];
            //var bufferGChandle = GCHandle.Alloc(readBuffer);
            if (outputTotalLength == 0x01)
            {
                outputStream.Write(readBuffer, 0, 1);
                return;
            }
            inputStream.Seek(offset, 0);
            int count;
            var totalCount = 0;
            var sendTimes = 0;
            //Note that while reading the size of the actual received data.
            while ((count = inputStream.Read(readBuffer, 0, readBuffer.Length)) != 0)
            {
                if (totalCount >= outputTotalLength)
                {
                    break;
                }
                if (count > outputTotalLength - totalCount)
                {
                    count = (int)(outputTotalLength - totalCount);
                }
                try
                {
                    totalCount += count;
                    outputStream.Write(readBuffer, 0, count);
                    outputStream.Flush();
                    sendTimes++;
                }
                catch (Exception ex)
                {
#if DEBUG
                    Console.WriteLine(ex.Message);
#endif
                }
#if DEBUG
                Console.WriteLine("The total length of the output: {4}, read position: {3}, sending Length: {0}, has sent a total length: {1}, {2} times sent", readBuffer.Length, totalCount, sendTimes, offset, outputTotalLength);
#endif
            }
            if (outputTotalLength - totalCount != 0)
            {
                outputStream.Write(readBuffer, 0, (int)(outputTotalLength - totalCount));
            }
            //bufferGChandle.Free();
            try
            {
                outputStream.Close();
            }
            catch (Exception ex)
            {
#if DEBUG
                Console.WriteLine(ex.Message);
#endif
            }
        }

        // Dispose() calls Dispose(true)
        /// <summary>
        /// Execution and release or resetting unmanaged resources associated application-defined tasks.
        /// </summary>
        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

        // NOTE: Leave out the finalize altogether if this class doesn't 
        // own unmanaged resources itself, but leave the other methods
        // exactly as they are. 
        /// <summary>
        /// Finalizes an instance of the <see cref="AirPlayLisenter" /> class.
        /// </summary>
        ~AirPlayLisenter()
        {
            // Finalize calls Dispose(false)
            Dispose(false);
        }

        // The bulk of the clean-up code is implemented in Dispose(bool)
        /// <summary>
        /// Releases unmanaged and - optionally - managed resources.
        /// </summary>
        /// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
        protected virtual void Dispose(bool disposing)
        {
            if (!disposing) return;
            // free managed resources
            if (_httpListener == null) return;
            _httpListener.Stop();
            _httpListener = null;
        }
    }
}
